Skip to main content
Glama

Convex MCP server

Official
by get-convex
settings.tsx22.8 kB
import { DeleteProjectModal } from "components/projects/modals/DeleteProjectModal"; import { PageContent } from "@common/elements/PageContent"; import { Loading } from "@ui/Loading"; import { Button } from "@ui/Button"; import { Sheet } from "@ui/Sheet"; import { LocalDevCallout } from "@common/elements/LocalDevCallout"; import { Callout } from "@ui/Callout"; import { DeploymentType } from "@common/features/settings/components/DeploymentUrl"; import { useDeployments } from "api/deployments"; import { useCurrentTeam, useTeamEntitlements } from "api/teams"; import { useCurrentProject } from "api/projects"; import { useCreateTeamAccessToken, useInstanceAccessTokens, useProjectAccessTokens, useProjectAppAccessTokens, useDeleteAppAccessTokenByName, } from "api/accessTokens"; import { useHasProjectAdminPermissions } from "api/roles"; import { useRouter } from "next/router"; import { useState, useEffect } from "react"; import { ProjectForm } from "components/projects/ProjectForm"; import { TrashIcon, InfoCircledIcon } from "@radix-ui/react-icons"; import { LostAccessCommand, LostAccessDescription, } from "components/projects/modals/LostAccessModal"; import { withAuthenticatedPage } from "lib/withAuthenticatedPage"; import { DefaultEnvironmentVariables } from "components/projectSettings/DefaultEnvironmentVariables"; import { getAccessTokenBasedDeployKey, getAccessTokenBasedDeployKeyForPreview, } from "components/deploymentSettings/DeployKeysForDeployment"; import { ProjectDetails } from "generatedApi"; import Link from "next/link"; import { useDeploymentUris } from "hooks/useDeploymentUris"; import Head from "next/head"; import { useAccessToken } from "hooks/useServerSideData"; import { MemberProjectRoles } from "components/projects/MemberProjectRoles"; import { DeploymentAccessTokenList } from "components/deploymentSettings/DeploymentAccessTokenList"; import { CustomDomains } from "components/projectSettings/CustomDomains"; import { TransferProject } from "components/projects/TransferProject"; import { cn } from "@ui/cn"; import { AuthorizedApplications } from "components/AuthorizedApplications"; import { Tooltip } from "@ui/Tooltip"; export { getServerSideProps } from "lib/ssr"; export default withAuthenticatedPage(function ProjectSettingsPage() { return ( <PageContent> <ProjectSettings /> </PageContent> ); }); const SECTION_IDS = { projectForm: "project-form", projectRoles: "project-roles", projectUsage: "project-usage", customDomains: "custom-domains", deployKeys: "deploy-keys", authorizedApps: "applications", envVars: "env-vars", lostAccess: "lost-access", transferProject: "transfer-project", deleteProject: "delete-project", } as const; const sections = [ { id: SECTION_IDS.projectForm, label: "Edit Project" }, { id: SECTION_IDS.projectRoles, label: "Project Admins" }, { id: SECTION_IDS.projectUsage, label: "Project Usage" }, { id: SECTION_IDS.customDomains, label: "Custom Domains" }, { id: SECTION_IDS.deployKeys, label: "Deploy Keys" }, { id: SECTION_IDS.authorizedApps, label: "Authorized Applications", }, { id: SECTION_IDS.envVars, label: "Environment Variables" }, { id: SECTION_IDS.lostAccess, label: "Lost Access" }, { id: SECTION_IDS.transferProject, label: "Transfer Project" }, { id: SECTION_IDS.deleteProject, label: "Delete Project" }, ]; function SettingsNavigation() { return ( <nav data-settings-nav className="relative" aria-label="Settings navigation" > <div className="absolute left-0 h-full w-0.5 rounded-sm bg-background-tertiary" aria-hidden="true" /> <SettingsNavigationScrollProgress /> <ul className="pl-1 text-sm"> {sections.map(({ id, label }) => ( <li key={id} className="py-px"> <a href={`#${id}`} className={cn( "block rounded-sm px-2 py-2 transition-all duration-200", "text-content-primary hover:bg-background-secondary", )} onClick={(e) => { e.preventDefault(); const element = document.getElementById(id); if (element) { const rect = element.getBoundingClientRect(); const isInView = rect.top >= 0 && rect.bottom <= window.innerHeight; element.scrollIntoView({ behavior: "smooth", block: isInView ? "start" : "nearest", inline: "nearest", }); window.history.pushState(null, "", `#${id}`); } }} > {label} </a> </li> ))} </ul> </nav> ); } function SettingsNavigationScrollProgress() { const [transform, setTransform] = useState<string | undefined>(undefined); useEffect(() => { const contentContainer = document.querySelector("[data-settings-content]"); if (!contentContainer) return undefined; const forceUpdate = () => { const containerRect = contentContainer.getBoundingClientRect(); const elementHeight = 1 / sections.length; const firstBoundary = findScrollBoundary("first", containerRect); const lastBoundary = findScrollBoundary("last", containerRect); const y = (firstBoundary.index + (1 - firstBoundary.visibilityFraction)) * elementHeight; const height = firstBoundary.index === lastBoundary.index ? firstBoundary.visibilityFraction * elementHeight : (firstBoundary.visibilityFraction + lastBoundary.visibilityFraction + lastBoundary.index - firstBoundary.index - 1) * elementHeight; setTransform(`translateY(${y * 100}%) scaleY(${height})`); }; forceUpdate(); // Initial calculation const update = () => { window.requestAnimationFrame(forceUpdate); }; contentContainer.addEventListener("scroll", update); window.addEventListener("resize", update); return () => { contentContainer.removeEventListener("scroll", update); window.removeEventListener("resize", update); }; }, []); if (transform === undefined) return null; return ( <div className="absolute left-0 h-full w-0.5 origin-top rounded-sm bg-content-primary" style={{ transform, }} aria-hidden="true" /> ); } function findScrollBoundary( boundary: "first" | "last", containerRect: DOMRect, ) { for ( let i = boundary === "first" ? 0 : sections.length - 1; boundary === "first" ? i < sections.length : i >= 0; boundary === "first" ? i++ : i-- ) { const section = sections[i]; const element = document.getElementById(section.id); if (!element) { continue; } const rect = element.getBoundingClientRect(); const visibleHeight = Math.min(rect.bottom, containerRect.bottom) - Math.max(rect.top, containerRect.top); if (visibleHeight > 0) { const elementHeight = rect.height; return { index: i, visibilityFraction: visibleHeight / elementHeight, }; } } return { index: 0, visibilityFraction: 0, }; } function ProjectSettings() { const team = useCurrentTeam(); const project = useCurrentProject(); const entitlements = useTeamEntitlements(team?.id); const hasAdminPermissions = useHasProjectAdminPermissions(project?.id); const router = useRouter(); const projectAppAccessTokens = useProjectAppAccessTokens(project?.id); const deleteAppAccessTokenByName = useDeleteAppAccessTokenByName({ projectId: project?.id, }); const authorizedAppsExplainer = ( <> <h3 className="mb-2">Authorized Applications</h3> <p className="text-sm text-content-primary"> These 3rd-party applications have been authorized to access this project on your behalf. </p> <div className="mt-2 mb-2 text-sm text-content-primary"> <span className="font-semibold"> What can authorized applications do? </span> <ul className="mt-1 list-disc pl-4"> <li>Create new deployments in this project</li> <li> <span className="flex items-center gap-1"> Manage this project <Tooltip tip="This includes actions like managing custom domains, managing environment variable defaults, and managing cloud backups and restores."> <InfoCircledIcon /> </Tooltip> </span> </li> <li> <span className="flex items-center gap-1"> Read and write data in any deployment in this project <Tooltip tip="Write access to Production deployments will depend on your team-level and project-level roles."> <InfoCircledIcon /> </Tooltip> </span> </li> </ul> </div> <p className="mt-1 mb-2 text-sm text-content-primary"> You cannot see applications that other members of your team have authorized. </p> {team && ( <p className="mt-1 mb-2 text-xs text-content-secondary"> There may also be <b>team-wide authorized applications</b> that can access all projects in this team. You can view them in{" "} <Link href={`/t/${team.slug}/settings/applications`} className="text-content-link hover:underline" > Team Settings </Link> . </p> )} </> ); useEffect(() => { // Handle initial scroll based on hash if (typeof window !== "undefined" && window.location.hash) { const id = window.location.hash.slice(1); // Remove the # from the hash const element = document.getElementById(id); if (element) { // Add a small delay to ensure the content is rendered setTimeout(() => { element.scrollIntoView({ behavior: "smooth", block: "start", inline: "start", }); }, 100); } } }, [team, project, router]); // Only run when team/project load since that's when content becomes available const title = <h2 className="pointer-events-auto py-6">Project Settings</h2>; return ( <> <Head> {project && ( <title>Project Settings | {project.name} | Convex Dashboard</title> )} </Head> <div className="relative h-full [--container-px:--spacing(6)] [--container-width:80rem] [--sidebar-gap:--spacing(8)] [--sidebar-width:12rem]"> <div className="pointer-events-none absolute inset-0 top-0 z-10 hidden md:block"> <div className="mx-auto flex h-full max-w-(--container-width) gap-(--sidebar-gap) px-(--container-px)"> <div className="h-full w-(--sidebar-width)"> <div className="grid h-full grid-rows-[auto_1fr]"> {title} <div className="scrollbar overflow-y-auto"> <div className="pointer-events-auto pb-8"> <SettingsNavigation /> </div> </div> </div> </div> <div className="grow" /> </div> </div> <div className="scrollbar h-full overflow-y-auto" data-settings-content> <div className="m-auto flex min-h-0 max-w-(--container-width) gap-(--sidebar-gap) px-(--container-px)"> <div className="hidden w-(--sidebar-width) shrink-0 md:block" /> <div className="flex flex-col items-start"> <div className="md:hidden">{title}</div> <div className="flex grow flex-col gap-6 pr-2 pb-6 md:pt-20 [&>*]:scroll-mt-3"> {team && project ? ( <div id={SECTION_IDS.projectForm}> <ProjectForm team={team} project={project} hasAdminPermissions={hasAdminPermissions} /> </div> ) : ( <Loading className="h-[50rem]" fullHeight={false} /> )} <div id={SECTION_IDS.projectRoles}> <MemberProjectRoles /> </div> {team && project && ( <Sheet id={SECTION_IDS.projectUsage}> <h3 className="mb-4">Project Usage</h3> <p className="text-sm"> View this project's usage and limits on{" "} <Link className="text-content-link hover:underline" href={`/t/${team.slug}/settings/usage?projectSlug=${project.slug}`} > this team's usage page </Link> . </p> </Sheet> )} {team && entitlements && ( <div id={SECTION_IDS.customDomains}> <CustomDomains team={team} hasEntitlement={ entitlements.customDomainsEnabled ?? false } /> </div> )} {project && ( <div id={SECTION_IDS.deployKeys}> <GenerateDeployKey project={project} hasAdminPermissions={hasAdminPermissions} /> </div> )} {project && ( <div id={SECTION_IDS.authorizedApps}> <AuthorizedApplications accessTokens={projectAppAccessTokens} explainer={authorizedAppsExplainer} onRevoke={async (token) => { await deleteAppAccessTokenByName({ name: token.name }); }} /> </div> )} <div id={SECTION_IDS.envVars}> <DefaultEnvironmentVariables /> </div> {team && project && !project?.isDemo && ( <div id={SECTION_IDS.lostAccess}> <LostAccess teamSlug={team.slug} projectSlug={project.slug} /> </div> )} <div id={SECTION_IDS.transferProject}> <TransferProject /> </div> <div id={SECTION_IDS.deleteProject}> <DeleteProject /> </div> </div> </div> </div> </div> </div> </> ); } function LostAccess({ teamSlug, projectSlug, }: { teamSlug: string; projectSlug: string; }) { return ( <Sheet> <h3 className="mb-4">Lost Access</h3> <LostAccessDescription /> <LostAccessCommand teamSlug={teamSlug} projectSlug={projectSlug} /> </Sheet> ); } function DeleteProject() { const router = useRouter(); const team = useCurrentTeam(); const project = useCurrentProject(); const hasAdminPermissions = useHasProjectAdminPermissions(project?.id); const [showDeleteModal, setShowDeleteModal] = useState(false); return ( <> <Sheet> <h3 className="mb-4">Delete Project</h3> <p className="mb-5 text-sm text-content-primary"> Permanently delete this project for you and all team members. This action cannot be undone. </p> <Button variant="danger" onClick={() => setShowDeleteModal(!showDeleteModal)} icon={<TrashIcon />} disabled={!hasAdminPermissions} tip={ !hasAdminPermissions ? "You do not have permission to delete this project." : undefined } > Delete </Button> </Sheet> {team && project && showDeleteModal && ( <DeleteProjectModal team={team} project={project} onClose={() => setShowDeleteModal(false)} onDelete={async () => { await router.push("/"); }} /> )} </> ); } function GenerateDeployKey({ project, hasAdminPermissions, }: { project: ProjectDetails; hasAdminPermissions: boolean; }) { return ( <Sheet className="flex flex-col gap-4"> <h3>Deploy Keys</h3> <div className="flex flex-col gap-4 divide-y"> <ProductionDeployKeys project={project} hasAdminPermissions={hasAdminPermissions} /> <PreviewDeployKeys project={project} /> </div> </Sheet> ); } function ProductionDeployKeys({ project, hasAdminPermissions, }: { project: ProjectDetails; hasAdminPermissions: boolean; }) { const team = useCurrentTeam(); const [accessToken] = useAccessToken(); const { deployments } = useDeployments(project.id); const prodDeployment = deployments?.find((d) => d.deploymentType === "prod"); const { prodHref } = useDeploymentUris(project.id, project.slug); const disabledReason = !hasAdminPermissions ? "CannotManageProd" : null; const accessTokens = useInstanceAccessTokens( disabledReason === null ? prodDeployment?.name : undefined, ); const createAccessTokenMutation = useCreateTeamAccessToken({ deploymentName: prodDeployment?.name || "", kind: "deployment", }); const deployKeyDescription = ( <p className="mb-2 text-sm text-content-primary"> This is the key for your{" "} <Link passHref href={prodHref} className="text-content-link"> <DeploymentType deploymentType="prod" /> deployment </Link> . Generate and copy this key to configure Convex integrations, such as automatically deploying to a{" "} <Link passHref href="https://docs.convex.dev/production/hosting" className="text-content-link" target="_blank" > hosting provider </Link>{" "} (like Netlify or Vercel) or syncing data with{" "} <Link passHref href="https://docs.convex.dev/database/import-export/streaming" className="text-content-link" target="_blank" > Fivetran or Airbyte </Link> . </p> ); return ( <div className="flex flex-col gap-2"> {team && prodDeployment ? ( <DeploymentAccessTokenList header="Production" description={deployKeyDescription} disabledReason={disabledReason} buttonProps={{ deploymentType: "prod", getAdminKey: async (name: string) => getAccessTokenBasedDeployKey( prodDeployment, project, team, `prod:${prodDeployment.name}`, accessToken, createAccessTokenMutation, name, ), disabledReason, }} identifier={prodDeployment.name} tokenPrefix={`prod:${prodDeployment.name}`} accessTokens={accessTokens} kind="deployment" /> ) : ( <div className="mb-4"> <h4 className="mb-2">Production</h4> <p className="text-sm text-content-primary"> This project does not have a Production deployment yet. </p> </div> )} </div> ); } function PreviewDeployKeys({ project }: { project: ProjectDetails }) { const createProjectAccessTokenMutation = useCreateTeamAccessToken({ projectId: project.id, kind: "project", }); const [accessToken] = useAccessToken(); const team = useCurrentTeam(); const selectedTeamSlug = team?.slug; const arePreviewDeploymentsAvailable = useTeamEntitlements(team?.id)?.projectMaxPreviewDeployments !== 0; const projectAccessTokens = useProjectAccessTokens(project.id); const deployKeyDescription = ( <p className="mb-2 text-sm text-content-primary"> These keys are for creating{" "} <Link passHref href="https://docs.convex.dev/production/hosting/preview-deployments" className="text-content-link" target="_blank" > preview deployments </Link> . Generate and copy a preview key to integrate Convex with a{" "} <Link passHref href="https://docs.convex.dev/production/hosting" className="text-content-link" target="_blank" > hosting provider </Link>{" "} (like Netlify or Vercel) in order to view both frontend and backend changes before they're deployed to production. </p> ); if (!arePreviewDeploymentsAvailable) { return ( <div className="pt-4"> <Callout> <p> <Link passHref href="https://docs.convex.dev/production/hosting/preview-deployments" className="underline" target="_blank" > Preview deployments </Link> {" are only available on the Pro plan. "} <Link href={`/${selectedTeamSlug}/settings/billing`} className="underline" > Upgrade to get access. </Link> </p> </Callout> <LocalDevCallout tipText="Tip: Run this to enable preview deployments locally:" command={`cargo run --bin big-brain-tool -- --dev grant-entitlement --team-entitlement project_max_preview_deployments --team-id ${team?.id} --reason "local" 200 --for-real`} /> </div> ); } return ( <div className="flex flex-col gap-2 pt-4"> {team && accessToken && createProjectAccessTokenMutation && ( <DeploymentAccessTokenList identifier={project.id.toString()} tokenPrefix={`preview:${selectedTeamSlug}:${project.slug}`} accessTokens={projectAccessTokens} kind="project" disabledReason={null} buttonProps={{ deploymentType: "preview", disabledReason: null, getAdminKey: async (name: string) => getAccessTokenBasedDeployKeyForPreview( project, team, `preview:${selectedTeamSlug}:${project.slug}`, accessToken, createProjectAccessTokenMutation, name, ), }} header="Preview" description={deployKeyDescription} /> )} </div> ); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server